﻿#region Copyright (c) 11/24/2011, Olaf Kober <amarok.projects@gmail.com>
//================================================================================
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//	copies of the Software, and to permit persons to whom the Software is
//	furnished to do so, subject to the following conditions:
//
//	The above copyright notice and this permission notice shall be included in
//	all copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//	THE SOFTWARE.
//================================================================================
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Amarok.Presentation.Resources;


namespace Amarok.Presentation
{
	/// <summary>
	/// This type provides a base class for view model implementations. This base class provides rich support for 
	/// property change notifications. If you need support for validation data errors too, you should derive from 
	/// base class <see cref="ValidatingViewModel"/> instead.
	/// </summary>
	public abstract class ViewModel :
		INotifyPropertyChanged
	{
		// data
		private readonly Lazy<PropertyChangedHandlerDictionary> mLazyPropertyChangedHandlers;
		private readonly Lazy<HashSet<String>> mLazyPropertyNames;
		private PropertyChangedEventHandler mPropertyChangedEvent;
		private Boolean mPropertyChangedEventSuspended;
		private List<String> mRecordedPropertyChangedEvents;
		private ViewModel mParent;


		#region ++ INotifyPropertyChanged Interface ++

		/// <summary>
		/// This event is raised when a property value changed. The property name in the event arguments indicates
		/// which property has changed. An empty string or null for the property name indicates that all of the 
		/// properties on this object have changed.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged
		{
			add
			{
				mPropertyChangedEvent += value;
			}
			remove
			{
				mPropertyChangedEvent -= value;
			}
		}

		#endregion

		#region ++ Public Interface (BeginRecording, EndRecording) ++

		/// <summary>
		/// Begins recording of property changed events. This starts a new empty recording session regardless of 
		/// whether <see cref="EndRecording"/> was called before.
		/// </summary>
		public void BeginRecording()
		{
			mRecordedPropertyChangedEvents = new List<String>();
		}

		/// <summary>
		/// Ends the recording of property changed events. This discards all recorded events. 
		/// <see cref="GetRecording"/> must be called before ending the recording session.
		/// </summary>
		public void EndRecording()
		{
			mRecordedPropertyChangedEvents = null;
		}

		/// <summary>
		/// Returns a list of property names that have been changed after recording has been started. This method
		/// must be called before ending the recording session via <see cref="EndRecording"/>.
		/// </summary>
		/// 
		/// <returns>
		/// A list of property names that have been changed, or null if the recording session has 
		/// already been ended.</returns>
		public List<String> GetRecording()
		{
			return mRecordedPropertyChangedEvents;
		}

		#endregion

		#region ++ Public Interface (BeginUpdate, EndUpdate) ++

		/// <summary>
		/// Suspends notifications about property changes. Neither registered event handler on 
		/// <see cref="PropertyChanged"/> nor registered custom property changed handler will be called.
		/// </summary>
		public void BeginUpdate()
		{
			mPropertyChangedEventSuspended = true;
		}

		/// <summary>
		/// Resumes notifications about property changes.
		/// </summary>
		public void EndUpdate()
		{
			mPropertyChangedEventSuspended = false;
		}

		#endregion

		#region ++ Public Interface (RaisePropertyChanged) ++

		/// <summary>
		/// Raises a property changed event that notifies that all of the properties on this object have changed. 
		/// This method can be called once after setting various view model properties to bulk-update the 
		/// data-bound view.
		/// </summary>
		public void RaisePropertiesChanged()
		{
			_NotifyPropertyChange(null);
		}

		/// <summary>
		/// Raises the property changed event to notify that a property on this object has changed.
		/// </summary>
		/// 
		/// <param name="propertyExpression">
		/// A lambda expression specifying the property that has changed.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression is not a member access expression.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to a property.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to an instance member.</exception>
		public void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
		{
			Verify.NotNull(propertyExpression, "propertyExpression");

			var propertyName = Helper.GetPropertyName(
				propertyExpression);

			_NotifyPropertyChange(propertyName);
		}

		/// <summary>
		/// Raises the property changed event to notify that a property on this object has changed.
		/// </summary>
		/// 
		/// <param name="propertyName">
		/// The name of the property that has changed. Optional, if left empty the name of the calling method,
		/// property or event is inserted. The property name is verified only in DEBUG and TEST builds.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// An empty string was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The type does not contain a public instance member property with specified name.</exception>
		public void RaisePropertyChanged(
			[CallerMemberName] String propertyName = null)
		{
			VerifyPropertyName(propertyName);
			_NotifyPropertyChange(propertyName);
		}

		#endregion

		#region ++ Public Interface (RegisterForPropertyChange) ++

		/// <summary>
		/// Registers the supplied handler method to be called when the specified property changed.
		/// </summary>
		/// 
		/// <param name="propertyExpression">
		/// A lambda expression specifying the property.</param>
		/// <param name="handler">
		/// The handler method that is called when the specified property changed.</param>
		/// 
		/// <returns>
		/// A disposable object that can be used to unregister the supplied handler.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression is not a member access expression.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to a property.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to an instance member.</exception>
		public IDisposable RegisterForPropertyChange<T>(Expression<Func<T>> propertyExpression,
			PropertyChangedEventHandler handler)
		{
			Verify.NotNull(propertyExpression, "propertyExpression");
			Verify.NotNull(handler, "handler");

			var propertyName = Helper.GetPropertyName(
				propertyExpression);

			return _RegisterForPropertyChange(propertyName, handler);
		}

		/// <summary>
		/// Registers the supplied handler method to be called when the specified property changed.
		/// </summary>
		/// 
		/// <param name="propertyName">
		/// The name of the property.</param>
		/// <param name="handler">
		/// The handler method that is called when the specified property changed.</param>
		/// 
		/// <returns>
		/// A disposable object that can be used to unregister the supplied handler.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// An empty string was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The type does not contain a public instance member property with specified name.</exception>
		public IDisposable RegisterForPropertyChange(String propertyName, PropertyChangedEventHandler handler)
		{
			VerifyPropertyName(propertyName);
			Verify.NotNull(handler, "handler");

			return _RegisterForPropertyChange(propertyName, handler);
		}

		#endregion

		#region ++ Public Interface (Parent) ++

		/// <summary>
		/// Gets a boolean value indicating whether this view model object has a parent object.
		/// </summary>
		public Boolean HasParent
		{
			get
			{
				return mParent != null;
			}
		}

		/// <summary>
		/// A reference to the parent object, or null if no parent object exists.
		/// </summary>
		public ViewModel Parent
		{
			get
			{
				return mParent;
			}
			set
			{
				mParent = value;

				this.RaisePropertyChanged("Parent");
				this.RaisePropertyChanged("HasParent");
			}
		}

		#endregion

		#region ## Protected Interface ##

		/// <summary>
		/// Initializes a new instance.
		/// </summary>
		/// 
		/// <param name="parent">
		/// A reference to the parent of this view model object.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		protected ViewModel(ViewModel parent)
			: this()
		{
			Verify.NotNull(parent, "parent");
			mParent = parent;
		}


		/// <summary>
		/// Initializes a new instance.
		/// </summary>
		protected ViewModel()
		{
			mLazyPropertyChangedHandlers = new Lazy<PropertyChangedHandlerDictionary>(
				() => new PropertyChangedHandlerDictionary(),
				false);

			mLazyPropertyNames = new Lazy<HashSet<String>>(
				_ResolvePropertyNames,
				false);
		}


		/// <summary>
		/// Verifies that this object contains a public instance member property with the specified name.
		/// </summary>
		/// 
		/// <param name="propertyName">
		/// The name of the property.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// An empty string was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The type does not contain a public instance member property with specified name.</exception>
		protected void VerifyPropertyName(String propertyName)
		{
			Verify.NotEmpty(propertyName, "propertyName");

			if (mLazyPropertyNames.Value.Contains(propertyName))
				return;

			var message = String.Format(CultureInfo.InvariantCulture,
				ExceptionResources.TypeDoesNotContainPublicInstanceProperty,
				this.GetType().FullName,
				propertyName);

			throw new ArgumentException(message, "propertyName");
		}

		#endregion

		#region ## Protected Interface (SetProperty) ##

		/// <summary>
		/// Assigns the given new value to the property, which backing field is supplied as reference value. The 
		/// property changed event is raised afterwards, but only if the new value is not equal to the old value.
		/// </summary>
		/// 
		/// <typeparam name="T">
		/// The type of the property, which should be set to the new value.</typeparam>
		/// 
		/// <param name="backingField">
		/// A reference to the backing field of the property.</param>
		/// <param name="newValue">
		/// The new value that should be assigned to the property.</param>
		/// <param name="propertyExpression">
		/// A lambda expression specifying the property that has changed.</param>
		/// <param name="forceOverwrite">
		/// A boolean value indicating whether the property changed event should be raised regardless whether 
		/// the value has changed or not. Defaults to false.</param>
		/// 
		/// <returns>
		/// A boolean value indicating whether the property changed event was raised.
		/// </returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression is not a member access expression.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to a property.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to an instance member.</exception>
		protected Boolean SetProperty<T>(
			Expression<Func<T>> propertyExpression,
			ref T backingField,
			T newValue,
			Boolean forceOverwrite = false)
		{
			Verify.NotNull(propertyExpression, "propertyExpression");

			if (forceOverwrite == false)
			{
				if (EqualityComparer<T>.Default.Equals(backingField, newValue))
					return false;
			}

			backingField = newValue;

			var propertyName = Helper.GetPropertyName(
				propertyExpression);

			_NotifyPropertyChange(propertyName);

			return true;
		}

		/// <summary>
		/// Assigns the given new value to the property, which backing field is supplied as reference value. The 
		/// property changed event is raised afterwards, but only if the new value is not equal to the old value.
		/// </summary>
		/// 
		/// <typeparam name="T">
		/// The type of the property, which should be set to the new value.</typeparam>
		/// 
		/// <param name="backingField">
		/// A reference to the backing field of the property.</param>
		/// <param name="newValue">
		/// The new value that should be assigned to the property.</param>
		/// <param name="propertyName">
		/// The name of the property that has changed. Optional, if left empty the name of the calling method,
		/// property or event is inserted. The property name is verified only in DEBUG and TEST builds.</param>
		/// <param name="forceOverwrite">
		/// A boolean value indicating whether the property changed event should be raised regardless whether 
		/// the value has changed or not. Defaults to false.</param>
		/// 
		/// <returns>
		/// A boolean value indicating whether the property changed event was raised.
		/// </returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// An empty string was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The type does not contain a public instance member property with specified name.</exception>
		protected Boolean SetProperty<T>(
			ref T backingField,
			T newValue,
			[CallerMemberName] String propertyName = null,
			Boolean forceOverwrite = false)
		{
			VerifyPropertyName(propertyName);

			if (forceOverwrite == false)
			{
				if (EqualityComparer<T>.Default.Equals(backingField, newValue))
					return false;
			}

			backingField = newValue;

			_NotifyPropertyChange(propertyName);

			return true;
		}

		#endregion

		#region Implementation

		private void _NotifyPropertyChange(String propertyName)
		{
			if (mPropertyChangedEventSuspended)
				return;

			_RecordPropertyChangedEvent(propertyName);
			_RaisePropertyChangedEvent(propertyName);
			_InvokePropertyChangedHandler(propertyName);
		}

		private void _RecordPropertyChangedEvent(String propertyName)
		{
			var list = mRecordedPropertyChangedEvents;

			if (list != null)
				list.Add(propertyName);
		}

		private void _RaisePropertyChangedEvent(String propertyName)
		{
			var handler = mPropertyChangedEvent;

			if (handler == null)
				return;

			handler(this, new PropertyChangedEventArgs(propertyName));

			#region (hook: property changed)
			ViewModelHooks.RaisePropertyChanged(this, propertyName);
			#endregion
		}

		private void _InvokePropertyChangedHandler(String propertyName)
		{
			if (mLazyPropertyChangedHandlers.IsValueCreated == false)
				return;

			_InvokePropertyChangedHandler(propertyName, new PropertyChangedEventArgs(propertyName));
		}

		private void _InvokePropertyChangedHandler(String propertyName, PropertyChangedEventArgs eventArgs)
		{
			if (propertyName == null)
			{
				foreach (var entry in mLazyPropertyChangedHandlers.Value)
					_InvokePropertyChangedHandler(entry.Key, eventArgs);
			}
			else
			{
				var list = mLazyPropertyChangedHandlers.Value.GetValueOrDefault(propertyName);

				if (list == null)
					return;

				var node = list.First;
				while (node != null)
				{
					node.Value.Raise(this, eventArgs);
					node = node.Next;
				}
			}
		}

		private IDisposable _RegisterForPropertyChange(String propertyName, PropertyChangedEventHandler handler)
		{
			var list = mLazyPropertyChangedHandlers.Value.GetValueOrDefault(propertyName);

			if (list == null)
			{
				list = new LinkedList<PropertyChangedHandlerRegistration>();
				mLazyPropertyChangedHandlers.Value.Add(propertyName, list);
			}

			var registration = new PropertyChangedHandlerRegistration();
			registration.Handler = handler;
			registration.List = list;
			registration.Node = list.AddLast(registration);

			return registration;
		}

		private HashSet<String> _ResolvePropertyNames()
		{
			var properties = this.GetType().GetProperties(
				BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);

			var results = new HashSet<String>();

			foreach (var property in properties)
				results.Add(property.Name);

			return results;
		}

		#endregion

		#region Implementation (Nested Types)

		private sealed class PropertyChangedHandlerRegistration :
			IDisposable
		{
			public PropertyChangedEventHandler Handler;
			public LinkedList<PropertyChangedHandlerRegistration> List;
			public LinkedListNode<PropertyChangedHandlerRegistration> Node;


			public void Raise(Object sender, PropertyChangedEventArgs e)
			{
				if (this.Handler != null)
					this.Handler(sender, e);
			}

			public void Dispose()
			{
				if (this.List != null && this.Node != null)
					this.List.Remove(this.Node);

				this.List = null;
				this.Node = null;
				this.Handler = null;
			}
		}

		private sealed class PropertyChangedHandlerDictionary :
			StringDictionary<LinkedList<PropertyChangedHandlerRegistration>>
		{
			public PropertyChangedHandlerDictionary()
				: base(StringComparer.Ordinal)
			{
			}
		}

		#endregion

	}
}
